package com.boot.project.core.service.impl;

import com.boot.project.core.service.CacheService;
import lombok.Data;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.PriorityQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 本地实现缓存的过期时间
 */
@Service
@ConditionalOnExpression("!'${cache.mode}'.equals('redis')")
public class LocalCacheService implements CacheService, InitializingBean {
    private static final ScheduledExecutorService swapExpiredPool
            = new ScheduledThreadPoolExecutor(10);

    private final ReentrantLock lock = new ReentrantLock();

    private final ConcurrentHashMap<String, Node> cache = new ConcurrentHashMap<>(1024);

    /**
     * 让过期时间最小的数据排在队列前，在清除过期数据时
     * ，只需查看缓存最近的过期数据，而不用扫描全部缓存
     *
     * @see Node#compareTo(Node)
     * @see SwapExpiredNodeWork#run()
     */
    private final PriorityQueue<Node> expireQueue = new PriorityQueue<>(1024);

    /**
     * 设置键值对，并更新过期时间
     *
     * @param key   键
     * @param value 值
     * @param ttl   过期时间，单位毫秒
     * @return
     */
    @Override
    public void setAndResetExpireTime(String key, Object value, long ttl) {
        Assert.isTrue(StringUtils.hasLength(key), "key can't be empty");
        Assert.isTrue(ttl > 0, "ttl must greater than 0");

        long expireTime = System.currentTimeMillis() + ttl;
        Node newNode = new Node(key, value, expireTime);
        lock.lock();
        try {
            Node old = cache.put(key, newNode);
            expireQueue.add(newNode);
            //如果该key存在数据，还要从过期时间队列删除
            if (old != null) {
                expireQueue.remove(old);
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 设置键值对，不更新过期时间
     *
     * @param key   键
     * @param value 值
     * @return
     */
    @Override
    public void setNotResetExpireTime(String key, Object value) {
        Assert.isTrue(StringUtils.hasLength(key), "key can't be empty");
        lock.lock();
        try {
            Node old = cache.get(key);
            Assert.isTrue(old != null, "old Node can't be null");
            old.setValue(value);
        } finally {
            lock.unlock();
        }
    }

    /**
     * 根据键获取值
     * @param key 键
     * @return
     */
    @Override
    public Object get(String key) {
        Node n = cache.get(key);
        if (n == null || n.expireTime < System.currentTimeMillis()) {
            return null;
        }
        return n.value;
    }

    /**
     * 根据键移除指定的键值对
     * @param key 键
     * @return
     */
    @Override
    public void remove(String key) {
        lock.lock();
        try {
            Node n = cache.remove(key);
            if (n != null) {
                expireQueue.remove(n);
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        // 使用默认的线程池，每1秒清除一次过期数据
        // 线程池和调用频率 最好是交给调用者去设置。
        swapExpiredPool.scheduleWithFixedDelay(
                new SwapExpiredNodeWork(), 0, 1, TimeUnit.SECONDS);
    }

    /**
     * 定时删除已经过期的数据
     */
    private class SwapExpiredNodeWork implements Runnable {

        @Override
        public void run() {

            long now = System.currentTimeMillis();
            while (true) {
                lock.lock();
                try {
                    Node node = expireQueue.peek();
                    // 没有数据了，或者数据都是没有过期的了
                    if (node == null || node.expireTime > now) {
                        return;
                    }
                    cache.remove(node.key);
                    expireQueue.poll();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    @Data
    public static class Node implements Comparable<Node> {
        private String key;
        private Object value;
        private long expireTime;

        public Node(String key, Object value, long expireTime) {
            this.value = value;
            this.key = key;
            this.expireTime = expireTime;
        }

        /**
         * @see SwapExpiredNodeWork
         */
        @Override
        public int compareTo(Node o) {
            long r = this.expireTime - o.expireTime;
            if (r > 0) {
                return 1;
            }
            if (r < 0) {
                return -1;
            }
            return 0;
        }
    }
}
